﻿//Copyright (c) 2010 Christopher E. S. King (http://chriskingconsulting.com)
//
//Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files 
// (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, 
// publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do 
// so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 
// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE 
// FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Reflection;
using System.Diagnostics.Contracts;
using System.Text.RegularExpressions;
using System.Collections;
using System.Globalization;
using System.IO;
using System.Xml;
using System.Xml.Linq;

using King.Extensions;
using System.Threading.Tasks;
using King.IO;
using System.Xml.Schema;

namespace King.Xml.IO {

    public struct XStreamChunk {
        private XDeclaration m_declaration;
        private XElement m_element;
        private TextReader m_text;
        private bool m_isEndElement;
        private bool m_isStartElement;

        public XStreamChunk(XDeclaration declaration) {
            this = default(XStreamChunk);

            m_declaration = declaration;
        }
        public XStreamChunk(TextReader text) {
            this = default(XStreamChunk);

            m_text = text;
        }
        public XStreamChunk(XElement element, bool isStartElement, bool isEndElement) {
            this = default(XStreamChunk);

            m_element = element;
            m_isStartElement = isStartElement;
            m_isEndElement = isEndElement;
        }
        
        public XDeclaration Declaration {
            get { return m_declaration; }
        }

        public XElement Element {
            get { return m_element; }
        }
        public bool IsText {
            get { return m_text != null; }
        }
        public bool IsElement {
            get { return m_element != null; }
        }
        public bool IsStartElement {
            get { return m_isStartElement; }
        }
        public bool IsEndElement {
            get { return m_isEndElement; }
        }

        public TextReader Text {
            get { return m_text; }
        }

        public void ConsumeSubtree() {
        }

        public override string ToString() {
            if (m_element != null)
                return m_element.ToString();

            if (m_declaration != null)
                return m_declaration.ToString();

            return "text";
        }
    }
    
    internal sealed class TextNodeReader : TextReaderBase {
        private XmlReader m_stream;
        private TextReader m_buffer;

        internal TextNodeReader(XmlReader stream) {
            m_stream = stream;
        }

        protected async override Task<int> FillBuffer(char[] buffer, int index, int count) {
            if (m_stream.CanReadValueChunk)
                return await m_stream.ReadValueChunkAsync(buffer, index, count);

            if (m_buffer == null)
                m_buffer = new StringReader(await m_stream.GetValueAsync());

            return await m_buffer.ReadAsync(buffer, index, count);
        }
    }

    public sealed class XStreamReader : IStreamableAsync<XStreamChunk> {

        private TextReader m_textReader;
        private Stack<XElement> m_stack;
        private XmlReader m_xmlReader;
        private EnumeratorAsync<XStreamChunk> m_enumerator;
        private TextNodeReader m_textNodeReader;
        private XDocument m_schema;

        public XStreamReader(TextReader textReader, XDocument schema = null) {
            m_textReader = textReader;
            m_schema = schema;
        }

        private static void ValidationCallBack(object sender, ValidationEventArgs e) {
            Console.WriteLine("Validation Error: {0}", e.Message);
        }

        private void SetSchemaValidation(XmlReaderSettings settings) {
            if (m_schema == null)
                return;

            // generate xsd
            var builder = new StringBuilder();
            using (var writer = new StringWriter(builder))
                m_schema.Save(writer);
            var xsd = builder.ToString();
            var reader = new StringReader(xsd);

            // add xsd to schema set
            var schemaSet = new XmlSchemaSet();
            schemaSet.Add(null, XmlReader.Create(reader));

            // validate against schmea
            settings.ValidationType = ValidationType.Schema;
            settings.ValidationFlags = XmlSchemaValidationFlags.ReportValidationWarnings;
            settings.ValidationEventHandler += ValidationCallBack;
            settings.Schemas = schemaSet;
        }
        private XmlReader CreateXmlReader() {

            var settings = new XmlReaderSettings { 
                // read stream asynchronously
                Async = true 
            };

            // validate against schema (if any)
            SetSchemaValidation(settings);

            // generate xml reader
            var result = XmlReader.Create(m_textReader, settings);

            return result;
        }

        // generator
        private IEnumerator<object> Generator() {
            m_stack = new Stack<XElement>();
            m_xmlReader = CreateXmlReader();

            while (true) {

                if (m_textNodeReader != null) {
                    m_textNodeReader.Close();
                    m_textNodeReader = null;
                }

                var result = m_xmlReader.ReadAsync();
                yield return (EnumeratorAsync.MoveNextResult)result;

                switch (m_xmlReader.NodeType) {
                    case XmlNodeType.Element:
                        yield return ReadElement();
                        break;

                    case XmlNodeType.EndElement:
                        yield return ReadEndElement();
                        break;

                    case XmlNodeType.Text:
                        yield return ReadText();
                        break;

                    case XmlNodeType.CDATA:
                        yield return ReadText();
                        break;

                    case XmlNodeType.XmlDeclaration:
                        yield return ReadDeclaration();
                        break;

                    case XmlNodeType.Whitespace:
                        break;

                    case XmlNodeType.SignificantWhitespace:
                        break;

                    default:
                        throw new NotSupportedException();
                }
            }
        }
        private void AllocateEnumerator() {
            if (m_enumerator == null)
                m_enumerator = new EnumeratorAsync<XStreamChunk>(Generator());
        }
        private XStreamChunk ReadDeclaration() {
            Contract.Assert(m_xmlReader.NodeType == XmlNodeType.XmlDeclaration);

            string version = null, encoding = null, standalone = null;

            while (m_xmlReader.MoveToNextAttribute()) {
                var name = m_xmlReader.Name;
                var value = m_xmlReader.Value;

                if (name == "encoding")
                    encoding = value;

                if (name == "version")
                    version = value;

                if (name == "standalone")
                    standalone = value;
            }

            return new XStreamChunk(new XDeclaration(version, encoding, standalone));
        }
        private XStreamChunk ReadElement() {
            Contract.Assert(IsStartElement);

            var element = ParseElement();

            m_stack.Push(element);

            if (m_xmlReader.IsEmptyElement)
                return ReadEndElement();

            return new XStreamChunk(element, IsStartElement, IsEndElement);
        }
        private XStreamChunk ReadEndElement() {
            Contract.Assert(IsEndElement);
            var element = m_stack.Pop();
            return new XStreamChunk(element, IsStartElement, IsEndElement);
        }
        private XStreamChunk ReadText() {
            return new XStreamChunk(m_textNodeReader = new TextNodeReader(m_xmlReader));
        }

        // helpers
        private bool IsStartElement {
            get {
                if (m_xmlReader == null)
                    return false;

                return m_xmlReader.NodeType == XmlNodeType.Element;
            }
        }
        private bool IsEndElement {
            get {
                if (m_xmlReader == null)
                    return false;

                return m_xmlReader.NodeType == XmlNodeType.EndElement || m_xmlReader.IsEmptyElement;
            }
        }
        private XAttribute ParseAttribute() {
            Contract.Assert(m_xmlReader.NodeType == XmlNodeType.Attribute);

            var ns = (XNamespace)m_xmlReader.NamespaceURI;
            var name = ns + m_xmlReader.LocalName;

            // default namespace is infered
            if (name == XNamespace.Xmlns + "xmlns")
                return null;

            var attribute = new XAttribute(name, m_xmlReader.Value);

            return attribute;
        }
        private XElement ParseElement() {
            Contract.Assert(m_xmlReader.NodeType == XmlNodeType.Element);

            var ns = (XNamespace)m_xmlReader.NamespaceURI;
            var name = ns + m_xmlReader.Name;

            var element = new XElement(name);

            while (m_xmlReader.MoveToNextAttribute())
                element.Add(ParseAttribute());

            m_xmlReader.MoveToElement();

            return element;
        }

        // IEnumeratorAsync<XStreamChunk>
        public IEnumeratorAsync<XStreamChunk> GetEnumerator() {
            if (m_enumerator != null)
                throw new ArgumentException("Only one enumerator can be generated per XTextReader.");
            return this;
        }
        IEnumeratorAsync IEnumerableAsync.GetEnumerator() {
            return GetEnumerator();
        }
        IEnumerator<XStreamChunk> IEnumerable<XStreamChunk>.GetEnumerator() {
            return GetEnumerator();
        }
        IEnumerator IEnumerable.GetEnumerator() {
            return GetEnumerator();
        }

        // IEnumerableAsync<XStreamChunk>
        public XStreamChunk Current {
            get {
                if (m_enumerator == null)
                    throw new InvalidOperationException();
                return m_enumerator.Current;
            }
        }
        object IEnumerator.Current {
            get { return Current; }
        }
        public Task<bool> MoveNextAsync() {
            AllocateEnumerator();
            return m_enumerator.MoveNextAsync();
        }
        public bool MoveNext() {
            AllocateEnumerator();
            return m_enumerator.MoveNext();
        }
        public void Reset() {
            AllocateEnumerator();
            m_enumerator.Reset();
        }
        public void Dispose() {
            AllocateEnumerator();
            m_enumerator.Dispose();
        }

        public async Task ConsumeSubtree() {
            var subtreeReader = m_xmlReader.ReadSubtree();
            while (await subtreeReader.ReadAsync())
                continue;
        }
    }
}
